package Zim::GUI;

use strict;
use vars qw/$AUTOLOAD %Config/;
use Carp;
use POSIX qw(strftime);
use File::BaseDir qw/
	xdg_config_home xdg_config_files
	xdg_data_home xdg_data_dirs xdg_data_files /;
use File::MimeInfo::Magic;
use Gtk2;
use Gtk2::Gdk::Keysyms;
use Gtk2::SimpleList;
use Zim;
use Zim::Events;
use Zim::Utils;
use Zim::GUI::Component;
use Zim::GUI::PathBar;
use Zim::GUI::PageView;

eval "use File::DesktopEntry; use File::MimeInfo::Applications";
my $has_mimeapplications = $@ ? 0 : 1;
warn "WARNING: Could not use 'File::MimeInfo::Applications', disabling application bindings\n"
	unless $has_mimeapplications;

our $VERSION = '0.20';
our @ISA = qw/Zim::Events Zim::GUI::Component/;

=head1 NAME

Zim::GUI - The application object for zim

=head1 SYNOPSIS

	use Zim::GUI;
	
	my $zim = Zim::GUI->new(\%SETTINGS);
	$zim->gui_init;
	$zim->gui_show;
	
	Gtk2->main;
	
	exit;

=head1 DESCRIPTION

This is developer documentation, for the user manual try
executing C<zim --doc>. For commandline options see L<zim>(1).

This module provides the application object for the Gtk2 application B<zim>.
The application has been split into several components for which the 
modules can be found in the Zim::GUI:: namespace. This object brings
together these components and manages the settings and data objects.

This object inherits from L<Zim::GUI::Component> and L<Zim::Events>.

=head1 METHODS

Undefined methods are AUTOLOADED either to an component or to the main
L<Gtk2::Window> object.

=over 4

=cut

our $CONFIG = # Default config values
q{
pane_pos	120
pane_vis	0
statusbar_vis	1
toolbar_vis	1
pathbar_type	recent
width		500
height		350
x
y
default_home	:Home
default_type	Files
browser
file_browser
email_client
undo_max	50
follow_new_link	0
backsp_unindent	1
show_spell	1
spell_language
cal_namespace	:Date:
show_cal	0
plugins
use_camelcase	1
use_utf8_ent	1
use_linkfiles	1
use_autolink	0
textfont
tearoff_menus	0
use_autoselect	1
follow_on_enter	1
use_ctrl_space	1
ro_cursor	0
expand_tree	0
save_interval	5000
};

our %DEFAULTS;
for (
	[file_browser => q/rox '%d'/],
	[browser	     => q/firefox '%u'/],
	[email_client => q/thunderbird '%u'/],
) {
	$DEFAULTS{$$_[0]} ||= $$_[1];
}

my $ui_menus =
# name		stock id 	label
## __actions
q{
FileMenu	.		_File
EditMenu	.		_Edit
ViewMenu	.		_View
SearchMenu	.		_Search
FormatMenu	.		For_mat
ToolsMenu	.		_Tools
GoMenu		.		_Go
HelpMenu	.		_Help
PathBarMenu	.		P_athbar type
};
##

my $ui_actions = 
# name,		stock id,	label,		accelerator,	tooltip
## __actions
q{
NewPage		gtk-new		_New Page	.		New page
popup_NewPage	gtk-new		_New Page	.		New page
OpenRep		gtk-open	_Open...	<ctrl>O		Open repository
Save		gtk-save	_Save		<ctrl>S		Save page
SaveCopy	gtk-save-as	S_ave A Copy...	<shift><ctrl>S	Save a copy
Export		.		E_xport...	.		Export
EmailPage	.		_Send To...	.		Mail page
RenamePage	.		_Rename Page...	F2		Rename page
popup_RenamePage	.		_Rename Page...	.		Rename page
DeletePage	.		_Delete Page	.		Delete page
popup_DeletePage	.		_Delete Page	.		Delete page
Props		gtk-properties	Proper_ties	.		Properties dialog
Close		gtk-close	_Close		<ctrl>W		Close window
Quit		gtk-quit	_Quit		<ctrl>Q		Quit
Search		gtk-find	_Search...	<shift><ctrl>F	Search
SearchBL	.		Search _Backlinks...	.	Search Back links
CopyLocation	.		Copy Location	<shift><ctrl>L	Copy location
Prefs		gtk-preferences	Pr_eferences	.		Preferences dialog
Reload		gtk-refresh	_Reload		<ctrl>R		Reload page
OpenFolder	gtk-open	Open _Folder	.		Open folder
RBIndex		.		Re-build Index	.		Rebuild index
GoBack		gtk-go-back	_Back		<alt>Left	Go page back
GoForward	gtk-go-forward	_Forward	<alt>Right	Go page forward
GoParent	gtk-go-up	_Parent		<alt>Up		Go to parent page
GoChild		gtk-go-down	_Child		<alt>Down	Go to child page
GoPrev		.		_Previous	<alt>Page_Up	Go to previous page
GoNext		.		_Next		<alt>Page_Down	Go to next page
GoToday		.		To_day		<alt>D		Today
GoHome		gtk-home	_Home		<alt>Home	Go home
JumpTo		gtk-jump-to	_Jump To...	<ctrl>J		Jump to page
ShowHelp	gtk-help	_Contents	F1		Help contents
ShowHelpKeys	.		_Keybindings	.		Key bindings
ShowHelpBugs	.		_Bugs		.		Bugs
About		gtk-about	_About		.		About
};
##

my $ui_toggle_actions =
# name,		stock id,	label,		accelerator,	tooltip 
## __actions
q{
TToolBar	.		Toolbar		.		Show toolbar
TStatusBar	.		_Statusbar	.		Show statusbar
TPane		gtk-index	Side _Pane	F9		Show side pane
TCalendar	stock_calendar-view-month	Calen_dar	<ctrl><shift>D	Show calendar
};
##

my $ui_radio_actions =
# name,		stock id,	label,		accelerator,	tooltip
## __actions
q{
PBRecent	.		_Recent pages	.		.
PBHistory	.		_History	.		.
PBNamespace	.		_Namespace	.		.
PBHidden	.		H_idden		.		.
};
##

my $ui_layout = q{<ui>
	<menubar name='MenuBar'>
		<menu action='FileMenu'>
			<menuitem action='NewPage'/>
			<menuitem action='OpenRep'/>
			<separator/>
			<menuitem action='Save'/>
			<menuitem action='SaveCopy'/>
			<menuitem action='Export'/>
			<separator/>
			<placeholder name='PrintActions'/>
			<menuitem action='EmailPage'/>
			<separator/>
			<placeholder name='PageMods'/>
			<separator/>
			<menuitem action='Props'/>
			<separator/>
			<menuitem action='Close'/>
			<menuitem action='Quit'/>
		</menu>
		<menu action='EditMenu'>
			<placeholder name='EditPage'/>
			<separator/>
			<menuitem action='CopyLocation'/>
			<separator/>
			<placeholder name='InsertItems'/>
			<separator/>
			<menuitem action='Prefs'/>
		</menu>
		<menu action='ViewMenu'>
			<menuitem action='TToolBar'/>
			<menuitem action='TStatusBar'/>
			<menuitem action='TPane'/>
			<menu action='PathBarMenu'>
				<menuitem action='PBRecent'/>
				<menuitem action='PBHistory'/>
				<menuitem action='PBNamespace'/>
				<menuitem action='PBHidden'/>
			</menu>
			<separator/>
			<menuitem action='TCalendar'/>
			<placeholder name='PluginItems'/>
			<separator/>
			<menuitem action='Reload'/>
		</menu>
		<menu action='FormatMenu'></menu>
		<menu action='SearchMenu'>
			<placeholder name='FindItems'/>
			<separator/>
			<menuitem action='Search'/>
			<menuitem action='SearchBL'/>
		</menu>
		<menu action='ToolsMenu'>
			<placeholder name='PageTools'/>
			<separator/>
			<menuitem action='OpenFolder'/>
			<separator/>
			<menuitem action='RBIndex'/>
		</menu>
		<placeholder name='PluginMenus'/>
		<menu action='GoMenu'>
			<menuitem action='GoBack'/>
			<menuitem action='GoForward'/>
			<menuitem action='GoParent'/>
			<separator/>
			<menuitem action='GoNext'/>
			<menuitem action='GoPrev'/>
			<separator/>
			<menuitem action='GoToday'/>
			<placeholder name='PluginItems'/>
			<separator/>
			<menuitem action='GoHome'/>
			<menuitem action='JumpTo'/>
		</menu>
		<menu action='HelpMenu'>
			<menuitem action='ShowHelp'/>
			<menuitem action='ShowHelpKeys'/>
			<menuitem action='ShowHelpBugs'/>
			<menuitem action='About'/>
		</menu>
	</menubar>
	<toolbar name='ToolBar'>
		<placeholder name='File'/>
		<separator/>
		<placeholder name='Edit'/>
		<separator/>
		<placeholder name='View'>
			<toolitem action='TPane'/>
		</placeholder>
		<separator/>
		<placeholder name='Go'>
			<toolitem action='GoHome'/>
			<toolitem action='GoBack'/>
			<toolitem action='GoForward'/>
		</placeholder>
		<separator/>
		<placeholder name='Search'/>
		<separator/>
		<placeholder name='Format'/>
		<separator/>
		<toolitem action='TCalendar'/>
		<placeholder name='Tools'/>
	</toolbar>
	<popup name='PagePopup'>
		<menuitem action='popup_NewPage'/>
		<menuitem action='popup_RenamePage'/>
		<menuitem action='popup_DeletePage'/>
	</popup>
	<accelerator action='GoChild'/>
</ui>};

my $ui_layout_rw = q{<ui>
<menubar name='MenuBar'>
		<menu action='FileMenu'>
			<placeholder name='PageMods'>
				<menuitem action='RenamePage'/>
				<menuitem action='DeletePage'/>
			</placeholder>
		</menu>
</menubar>
</ui>};

our $CURRENT;

=item C<new(profile => NAME, settings => \%SETTINGS)>

Simple constructor.

=cut

sub init {
	my $self = shift;
	# Initialize attributes
	$self->{app} = $self; # fool base class
	$self->{_message_timeout} = -1;
	$self->{_save_timeout}    = -1;
	
	# load repository
	my $repository = $self->{repository}
		|| Zim->new(
			dir      => $$self{dir},
			type     => $$self{type},
		);
	$self->{history} = $repository->init_history();
	my $dir = $repository->{dir}; # FIXME what if there is no dir

	$self->{name} ||= $repository->config->{name};
	unless ($self->{name}) { # needed for trayicon tooltip
		my @dirs = grep length($_),
			File::Spec->splitdir($dir);
		$self->{name} = pop @dirs;
	}
	$repository->config->{name} = $self->{name};
	warn "# Repository name: $self->{name}\n";

	$self->{profile} ||= $repository->config->{profile} || 'default';

	if (my $icon = $repository->config->{icon}) {
		my $file = File::Spec->rel2abs($icon, $dir);
		($file) = xdg_data_files('pixmaps', $icon) unless -f $file;
		$self->{icon_file} = $file if defined $file and -f $file;
	}

	$self->{repository} = $repository;
	$repository->{user_name} = $self->{settings}{user_name};

	# Load config
	$self->load_config;
	$self->set_home();

	return $self;
}

=item C<gui_init()>

This method initializes all GUI objects that make up the application.

=cut

sub gui_init {
	my $self = shift;

	## Setup the window
	my $window = Gtk2::Window->new('toplevel');
	$window->set_default_size(@{$self->{settings}}{'width', 'height'});
	$window->signal_connect_swapped(delete_event => \&on_delete_event, $self);
	$window->signal_connect(destroy => sub { Gtk2->main_quit });
#	if (	defined $self->{icon_file}
#		or ! Gtk2->CHECK_VERSION(2, 6, 0)
#	) {
		$window->set_icon(
			Gtk2::Gdk::Pixbuf->new_from_file(
				$self->{icon_file} || $Zim::ICON) );
#	}
#	else {
#		$window->set_icon_name('zim'); # since 2.6.0
#	}
	$window->set_title('Zim');
	$self->{window} = $window;

	$SIG{'USR1'} = sub { # toggle window on `kill -USR1`
		my $iconified = grep {$_ eq 'iconified'}
			$window->window->get_state;
		$iconified ? $window->present : $window->iconify ;
	}; # defined _before_ plugins are loaded - so TrayIcon can change this
	
	#$window->signal_connect(hide => sub { # FIXME does not work - patched in TrayIcon
	#		@{$self->{settings}}{'x', 'y'} = $window->get_position;
	#		warn "hiding window: @{$self->{settings}}{'x', 'y'}\n";
	#	} );
	$window->signal_connect(show => sub {
			my ($x, $y) = @{$self->{settings}}{'x','y'};
			#warn "showing window: $x, $y\n";
			$window->move($x,$y) if defined($x) and defined ($y);
		} );
			

	my $vbox = Gtk2::VBox->new(0, 0);
	$window->add($vbox);
	$self->{vbox} = $vbox;

	## Setup actions and ui for menubar and toolbar
	my $ui = Gtk2::UIManager->new;
	$window->add_accel_group($ui->get_accel_group);
	$ui->set_add_tearoffs($self->{settings}{tearoff_menus});
	$self->{ui} = $ui;
	
	$self->add_actions($ui_menus, 'MENU');
	$self->add_actions($ui_actions);
	$self->add_actions($ui_toggle_actions, 'TOGGLE');
	$self->add_actions($ui_radio_actions, 'RADIO', 'TPathBar');
	
	$self->actions_set_sensitive(
		Save      => $self->{settings}{read_only} ? 0 : 1,
		GoBack    => 0,
		GoForward => 0,
		GoParent  => 0,
	);
	
	$self->add_ui($ui_layout);
	$self->add_ui($ui_layout_rw) unless $self->{settings}{read_only};
	
	$self->{menubar} = $ui->get_widget("/MenuBar");
	$vbox->pack_start($self->{menubar}, 0,1,0);
	$self->{toolbar} = $ui->get_widget("/ToolBar");
	$vbox->pack_start($self->{toolbar}, 0,1,0);
	$self->{toolbar}->hide;
	$self->{toolbar}->set_no_show_all(1);

	## General window layout
	my $hpaned = Gtk2::HPaned->new();
	$hpaned->set_position($self->{settings}{pane_pos});
	$vbox->pack_start($hpaned, 1,1,0);
	$self->{hpaned} = $hpaned;

	my $l_vbox = Gtk2::VBox->new(0, 0);
	$hpaned->add1($l_vbox);
	$l_vbox->set_no_show_all(1);
	$self->{l_vbox} = $l_vbox;

	my $r_vbox = Gtk2::VBox->new(0, 0);
	$hpaned->add2($r_vbox);
	$self->{r_vbox} = $r_vbox;

	my $shbox = Gtk2::HBox->new(0, 2);
	$shbox->set_no_show_all(1);
	$vbox->pack_start($shbox, 0,1,0);
	$self->{statusbar} = $shbox;
	
	## Status bar
	my $s1 = Gtk2::Statusbar->new;
	$s1->set_has_resize_grip(0);
	$shbox->pack_start($s1, 1,1,0);
	$self->{status1} = $s1;

	my $frame = Gtk2::Frame->new();
	$frame->set_shadow_type('in');
	$s1->pack_end($frame, 0,1,0);
	my $ebox = Gtk2::EventBox->new();
	$frame->add($ebox);
	my $l1 = Gtk2::Label->new();
	$l1->set_size_request(120,10);
	$l1->set_alignment(0, 0.5);
	$l1->set_mnemonic_widget( ($self->get_action('SearchBL')->get_proxies)[0] );
	$ebox->add($l1);
	$self->{statusl} = $l1;
	
	my $s2  = Gtk2::Statusbar->new;
	$s2->set_size_request(80,10);
	$shbox->pack_end($s2, 0,1,0);
	$self->{status2} = $s2;
	
	$ebox->signal_connect_swapped(button_press_event => \&on_click_backlinks, $self);

	## Build side pane
	$self->{autoload}{TreeView} = sub {
		my %args = @_;
		eval "require Zim::GUI::TreeView";
		die $@ if $@;
		my $tree_view = Zim::GUI::TreeView->new(%args);
		$tree_view->load_index();
		$tree_view->signal_connect(row_activated =>
			sub { $self->TPane(0) if $self->{_pane_visible} == -1 } );
		$l_vbox->add($tree_view->widget);
		return $tree_view;
	};

	## Build content of the main area
	my $path_bar = Zim::GUI::PathBar->new(app => $self);
	$r_vbox->pack_start($path_bar->widget, 0,1,5);
	$self->{objects}{PathBar} = $path_bar;

	my $page_view = Zim::GUI::PageView->new(app => $self);
	$r_vbox->pack_end($page_view->widget, 1,1,0);
	$self->{objects}{PageView} = $page_view;

	$vbox->set_focus_chain($l_vbox, $page_view->widget);
		# get pathbar and toolbar out of the loop
	
	my $hist = $self->{history};
	$hist->set_GUI($page_view) if $hist;

	## Some wiring
	my $accels = $ui->get_accel_group;
	# Ctrl-Space / Alt-Space
	my @combo = ([ord(' '), ['mod1-mask']]);
	push @combo, [ord(' '), ['control-mask']] if $self->{settings}{use_ctrl_space};
	$accels->connect( 
		@$_, ['visible'],
		sub {
			if ($self->{settings}{pane_vis}) {
				my $tree_view = $self->TreeView;
				my $page_view = $self->PageView;
				if ($tree_view->has_focus) { $page_view->grab_focus }
				else                       { $tree_view->grab_focus }
			}
			else {
				my $vis = $self->{_pane_visible} ? 0 : -1;
				$self->TPane($vis);
			}
		} ) for @combo;
	$accels->connect( Gtk2::Accelerator->parse('F5'), ['visible'],
		sub { $self->Reload } );

	$self->signal_connect('page_loaded', sub {
		my $self = shift;
		my $state = $hist->get_state;
		$self->actions_set_sensitive(
			GoBack     => ($$state{back} ? 1 : 0),
			GoForward  => ($$state{forw} ? 1 : 0),
			GoParent   => ($self->{page}->namespace =~ /[^:]/),
		)
	} ) if $hist;

	## List autoloaded components
	$self->{autoload}{$_} = 'Zim::GUI::'.$_
		for qw/ SearchDialog FindReplaceDialog
		        Calendar ExportDialog PreferencesDialog
			PropertiesDialog RepositoriesDialog/ ;

	## Load plugins
	$self->plug($_) for grep length($_), split /,/, $self->{settings}{plugins};

	## Try saving the program on system signals
	for my $sig (qw/TERM HUP PIPE/) {
		$SIG{$sig} = sub {
			$self->Quit();
			$self->exit_error(undef,"Signal $sig caught\n");
		};
	}

	# Try saving on desktop exit
#	$window->get_display->signal_connect( closed => sub {
#			print "Display closed\n";
#		} );
}

=item C<gui_show()>

=cut

sub gui_show {
	my $self = shift;
	
	## Show all widgets .. well most of them
	$self->{ui}->ensure_update; # make sure the menu is complete
	$self->{window}->show_all;
	$self->{statusbar}->set_no_show_all(0);
	$self->{l_vbox}->set_no_show_all(0);
	$self->{toolbar}->set_no_show_all(0);
#	my ($x, $y) = @{$self->{settings}}{'x','y'};
#	$window->move($x,$y) if defined($x) and defined ($y);
	
	## Find a page to load
	my $hist = $self->{history};
	unless ($self->{page}) {
		my $page = $hist ? $hist->get_current : undef;
		$page ||= $self->{home};
		$page = ref($page)
			? $page
			: $self->{repository}->resolve_page($page);
		$self->load_page($page);
	}
	
	## Toggle some widgets
	$self->{message_lock} = 1;
	my $set = $self->{settings};
	my $pbtype = lc $$set{pathbar_type};
	$pbtype = 'recent' unless grep {$_ eq $pbtype}
		qw/recent history namespace hidden/;
	$$set{pathbar_type} = $pbtype;
	$self->actions_set_active(
		TToolBar   => $$set{toolbar_vis},
		TStatusBar => $$set{statusbar_vis},
		TPane      => $$set{pane_vis},
		TCalendar  => $$set{show_cal},
	);
	$self->TPathBar($pbtype);
	$self->{message_lock} = 0;

	## set autosave interval - default is every 5 seconds
	my $t = $$set{save_interval} || 5000;
	$self->{_save_timeout} = 
		Glib::Timeout->add($t, sub {$self->SaveIfModified('SOFT'); 1})
		unless $self->{settings}{read_only};
	
	$self->signal_emit('gui_show');
	$self->PageView->grab_focus;
}


sub DESTROY {
	my $self = shift;
	for (qw/_save_timeout _message_timeout/) {
		my $timeout = $self->{$_};
		Glib::Source->remove($timeout) if $timeout >= 0;
	}
}

sub AUTOLOAD {
	my $self = shift;
	my $class = ref $self;
	$AUTOLOAD =~ s/^.*:://;
	
	return if $AUTOLOAD eq 'DESTROY';
	croak "No such method: $class::$AUTOLOAD" unless ref $self;
	#warn join ' ', "Zim::AUTOLOAD called for $AUTOLOAD by: ", caller, "\n";
	
	if (exists $self->{objects}{$AUTOLOAD}) {
		return $self->{objects}{$AUTOLOAD};
	}
	elsif ($AUTOLOAD =~ /^on_(\w+)$/) { # could be an action handler
		no strict 'refs';
		goto &{"Zim::GUI::Component::$AUTOLOAD"}; # call parent
	}
	elsif (exists $self->{autoload}{$AUTOLOAD}) {
		# Delayed initialization of components
		#warn "Autoloading: $AUTOLOAD\n";
		my $obj = delete $self->{autoload}{$AUTOLOAD};
		eval {
			if (ref($obj) eq 'CODE') { $obj = $obj->(app => $self) }
			else { # expect $load to be a class
				eval "require $obj";
				die $@ if $@;
				$obj = $obj->new(app => $self);
			}
		};
		$self->exit_error($@) if $@;
		$self->{objects}{$AUTOLOAD} = $obj;
		return $obj
	}
	
	# Call method on default widget
	croak "No such method: $class::$AUTOLOAD"
		unless $self->{window}->can($AUTOLOAD);
	return $self->{window}->$AUTOLOAD(@_);
}

=item C<set_profile(NAME)>

Load profile NAME.

=cut

sub set_profile {
	my ($self, $name) = @_;
	# TODO:
	# reload config
	# toggle visible elements
	# toggle read-only if wanted
	$self->{profile} = lc $name;
}

=item C<set_home(PAGE)>

Sets the new home page.

=cut

sub set_home {
	my ($self, $home) = @_;
	my $rep = $self->{repository};
	$home ||= $rep->config->{home}
	      || $self->{settings}{default_home};
	my $name = $rep->resolve_name($home);
	$rep->config->{home} = $name;
	$self->{home} = $name;
	warn "# Home: $name\n";
}

sub on_delete_event {
	# Called when the window is deleted
	# return true to avoid destruction
	my $self = shift;
	my $force = uc(shift) eq 'FORCE';

	eval {
		my ($settings, $window) = @{$self}{'settings', 'window'};
		@{$settings}{'width', 'height'} = $window->get_size;
		@{$settings}{'x', 'y'} = $window->get_position if $window->visible;
			# position is 0,0 when window is hidden
	};
	
	if ($self->{hide_on_delete} and !$force) {
		$self->{window}->hide;
		return 1;
	}
	
	$self->SaveIfModified() || return 1;
	$self->save_config;

	$self->{history}->write if $self->{history};

	return 0;
}

=item C<current()>

=cut

sub current { return $CURRENT }

=item C<plug(PLUGIN)>

=item C<unplug(PLUGIN)>

=cut

sub plug { # TODO add plugin to config
	my ($self, $name) = @_;
	return $self->error_dialog("Plugin already loaded: $name")
		if $self->{objects}{$name};
		
	my ($script) = xdg_data_files('zim', 'plugins', $name.'.pl');
	unless (length $script) {
		$self->unplug($name);
		return $self->error_dialog("No such plugin: $name");
	}

	#warn "Loading plugin $name from $script\n";
	local $CURRENT = $self;
	my $return = do $script;
	if ($@ or ! defined $return) {
		$self->unplug($name);
		return $self->error_dialog("Failed to load $name plugin", $@ || $!);
	}

	return 1;
}

sub unplug {
	my ($self, $name) = @_;
	$self->{settings}{plugins} =~ s/\Q$name\E,*//;
	$self->del_object($name);
}

=item C<< add_object(NAME => OBJECT) >>

Add a child object to the application object.
To access this object later you can use NAME as a method on the application
object. By convention object names should be written in CamelCase to avoid
conflict with normal methods. Be aware that actions also use CamelCase though.

OBJECT can either be an object reference, a class name or a code reference.
In case of a class reference the object is autoloaded with
C<< CLASS->new(app => $app) >>, where C<$app> is the main application obejct.
In case of a code reference this code is expected to return an object reference
when we try to autoload the object.

=item C<del_object(NAME)>

Remove a child object from the application object.
Does not necessarily destroy the child object.

=cut

sub add_object {
	my ($self, $name, $obj) = @_;
	croak "Object '$name' already exists"
		if exists $self->{objects}{$name}
		or exists $self->{autoload}{$name} ;
	croak "Object '$name' conflicts with action name"
		if $self->can($name);
	if (! ref $obj or ref($obj) eq 'CODE') {
		$self->{autoload}{$name} = $obj;
	}
	else {
		$self->{objects}{$name} = $obj;
	}
	return 1;
}

sub del_object {
	delete ${$_[0]{autoload}}{$_[1]};
	return delete ${$_[0]{objects}}{$_[1]};
}

sub on_click_backlinks {
	my ($self, $event) = @_;
	return if $event->type ne 'button-press';
	
	if ($event->button == 1) {
		$self->SearchDialog->{dialog}
			? $self->SearchDialog->hide
			: $self->SearchBL  ;
	}
	return unless $event->button == 3;
	
	my @blinks = $self->{page}->list_backlinks;
	return unless @blinks;
	
	my $menu = Gtk2::Menu->new();
	for my $l (@blinks) {
		my $item = Gtk2::ImageMenuItem->new_with_label($l);
		$item->signal_connect(activate =>
			sub { $self->load_page($l) }  );
		$menu->add($item);
	}

	$menu->show_all;
	
	my ($button, $time) = $event
		? ($event->button, $event->time)
		: (0, 0) ;
	$menu->popup(undef, undef, undef, undef, $button, $time);
}

=item C<widget()>

Returns the root window widget.
Use this widget for things like show_all() and hide_all().

=cut

sub widget { return $_[0]->{window} }

=item C<load_config>

Read config file.

=cut

sub load_config {
	my $self = shift;
	my $p = $self->{profile};

	# load defaults
	for (split /\n/, $CONFIG) {
		next unless /\S/;
		my ($key, $val) = split /\s+/, $_, 2;
		$$self{settings}{$key} = $val
	}
	$$self{settings}{user_name} ||= $ENV{USER};

	# load file
	my $file = _xdg_config_data_files('zim', "$p.conf");
	$file = _xdg_config_data_files('zim', 'default.conf') unless $file;
	return warn "WARNING: Could find default configuration\n" unless $file;
	warn "# Profile: $p\n# Using config from: $file\n";
	$file->read_config($self->{settings});

	# load accelerators
	my $accelmap = _xdg_config_data_files('zim', 'accelmap');
	Gtk2::AccelMap->load($accelmap) if $accelmap;
}

sub _xdg_config_data_files {
	# We mix up XDG basedir spec by saving in XDG_CONFIG_HOME
	# but loading for XDG_DATA_DIRS
	my @path = @_;
	my $file = Zim::File->new(xdg_config_home, @path);
	return $file if $file->exists;
	($file) = xdg_data_files(@path);
	return Zim::File->new($file) if defined $file;
	return undef;
}

=item C<save_config>

Save config file.

=cut

sub save_config {
	my $self = shift;
	$self->{settings}{pane_pos} = $self->{hpaned}->get_position;
	my $p = $self->{profile};
	my %conf = %{$self->{settings}};
	my $file = Zim::File->new(xdg_config_home, 'zim', "$p.conf");
	warn "# Writing config to: $file\n";
	$file->write_config(\%conf);

	my $accelmap = File::Spec->catfile(
		xdg_config_home, 'zim', 'accelmap' );
	Gtk2::AccelMap->save($accelmap);
}

=item C<link_clicked(LINK)>

Loads a page in zim or opens an external url in a browser.

LINK is considered to be either an page name, an url or a file name.
Page names are resolved as relative links first.
Dispatches to C<open_file()> or C<open_url()> when
LINK is a file or an url.

=cut

sub link_clicked {
	my ($self, $link) = @_;
	$link =~ s/^\s+|\s+$//g; # no whitespace pre- or post-fix
	#warn "link clicked: >>$link<<\n";
	return warn "WARNING: You tried to folow an empty link.\n"
		unless length $link;

	my ($type, $l) = $self->{page}->parse_link($link);
	return $self->error_dialog("Link undefined: $link")
	       unless defined $type;
	
	if ($type eq 'page') {
		my $name = $self->{repository}->resolve_name($l, $self->{page});
		$self->load_page($name);
	}
	elsif ($type eq 'file') { $self->open_file($l)         }
	elsif ($type eq 'man')  { $self->ShowHelp(':man:'.$l)  }
	else                    { $self->open_url($l, $type)   }
}

=item C<open_file(FILE)>

Opens FILE with the apropriate program.
Calls C<open_directory()> when FILE is a directory.

=cut

sub open_file {
	my ($self, $file) = @_;
	return $self->open_directory($file) if $file =~ /\/$/ or -d $file;
	my $mt = mimetype($file);
	
	return $self->open_directory($file, 'OPEN_FILE')
		unless $mt and $has_mimeapplications;
	
	my ($app) = grep $_, mime_applications($mt);
		# if no default, select first
		# FIXME: ask user for default

	return $self->open_directory($file, 'OPEN_FILE') unless $app;

	unless (fork) { # child process
		warn 'Executing '.$app->get_value('Name')." on $file\n";
		eval { $app->exec($file) };
		exit 1;
	}
}

=item C<open_directory(DIR)>

Opens DIR in the file browser. Open the parent directory when
DIR turns out to be a file.

=cut

sub open_directory {
	my ($self, $dir, $open_file) = @_; # open_file is a private arg
	
	my $browser = $self->{settings}{file_browser};
	unless ($browser) { # prompt for a browser
		my $val = $self->run_prompt(
			'Choose file browser',
			['cmd'], {cmd => ['Command', 'string', $DEFAULTS{file_browser}]},
			undef, undef,
			'Please enter a file browser' );
		($browser) = @$val;
		$browser = undef unless $browser =~ /\S/;
		return $self->error_dialog('You have no browser configured')
			unless defined $browser;
		$self->{settings}{file_browser} = $browser;
	}
	
	if ( ! -d $dir and (! $open_file or $browser =~ /\%d/) ) {
		# strip filename if $dir is a file
		# we default to %f is $open_file
		my ($vol, $dirs, undef) = File::Spec->splitpath($dir);
		$dir = File::Spec->catpath($vol, $dirs);
	}
	
	$dir = Zim::File->localize($dir);
	$browser =~ s/\%[sfd]/$dir/ or $browser .= " \"$dir\"";
	Zim::Utils->run($browser);
}

=item C<open_url(URL)>

Opens URL in the web browser.

=cut

sub open_url {
	my ($self, $url, $type) = @_;
	
	$url =~ /^(\w[\w\+\-\.]+):/
		or return $type
			? $self->error_dialog("Can't find $type?$url")
			: $self->error_dialog("Not an url: $url") ;
	my $proto = $1;
	
	if ($proto eq 'zim') { # special case
		$url =~ s#\?(.*)$##;
		my $page = $1;
		$url =~ s#^zim:#file:#i;
		my $dir = Zim::File->parse_uri($url);
		$dir = Zim::File->localize($dir);
		return $self->exec_new_window($dir, $page);
	}
	
	my ($app, $title, $string, $error) = ($proto eq 'mailto')
		? (
			'email_client',
			'Choose Email Client',
			'Please enter an email client',
			'You have no email client configured'
		) : (
			'browser',
			'Choose Browser',
			'Please enter a browser to open external links',
			'You have no browser configured'
		) ;

	my $browser = $self->{settings}{$app};
	$browser ||= $ENV{BROWSER} unless $proto eq 'mailto';
	unless ($browser) { # prompt for a browser
		my $val = $self->run_prompt(
			$title,
			['cmd'], {cmd => ['Command', 'string', $DEFAULTS{$app}]},
			undef, undef,
			$string ) || [] ;
		($browser) = @$val;
		$browser = undef unless $browser =~ /\S/;
		return $self->error_dialog($error)
			unless defined $browser;
		$self->{settings}{$app} = $browser;
	}
	
	$browser =~ s/\%[us]/$url/ or $browser .= " \"$url\"";
	Zim::Utils->run($browser);
}

=item C<load_page(PAGE)>

Loads a new page, updates history etc. when necessary.
PAGE should be either an absolute page name, a history record or a page object.

use C<link_clicked()> for relative page names, urls etc.

=cut

sub load_page {
	my ($self, $page) = @_;

	$self->SaveIfModified or return
		if $self->{page};

	goto &_load_page;
}

sub _load_page { # load _without_ saving first
	my ($self, $page) = @_;
	my $prevpage = $self->{page};
	
	# See if we got a name, a page or a history record
	my ($rec, $name);
	($name, $page, $rec) = 
		(ref($page) eq 'HASH') ? ($page->{name}, undef, $page) :
		 ref($page)            ? ($page->name,   $page, undef) : ($page, undef, undef) ;
	warn "## GUI::load_page $name obj: $page rec: $rec\n";
	
	# Get page if we didn't get it as argument
	unless ($page) {
		return warn "WARNING: You tried to load an empty name.\n"
			unless length $name;
		
		eval { $page = $self->{repository}->get_page($name) };
		return $self->error_dialog("Could not load page $name\n\n$@", $@) if $@;
	}
	if (! defined $page or
		($self->{settings}{read_only} and ! $page->exists)
	) {
		$self->error_dialog("Page does not exist:\n$name");
		if (defined $self->{page}) { return }
		else { # No previous page - we need to show something
			$page ||= Zim::Page->new($self->{repository}, $name);
			$page->set_source(undef); # orphane page
		}
	}
	
	# Update history if we didn't get a record as argument
	# if we did get a record it is from GoBack() or GoForw()
	# and the history is updated already
	unless ($rec) { # get history record
		$rec = $self->{history}->set_current($name)
			if $self->{history};
	}
	$self->{page} = $page;

	# Actually load the page in the GUI
	eval { $self->PageView->load_page($page) };
	if ($@) {
		$self->exit_error("Could not load page $name\n\n$@", $@);
		# TODO handle more gratiously .. needs read_only per page
	}
	
	# Update various objects
	$self->{objects}{PageView}->set_state($rec->{state})
		if $rec and $rec->{state};
	$self->{history}->delete_recent($prevpage->name)
		if   $self->{history}
		and (defined $prevpage and ! $prevpage->exists);
	$self->signal_emit('page_loaded', $name);
	
	$self->{window}->set_title("$name - Zim");
	$self->update_status();
	my $links = scalar $page->list_backlinks;
	$self->{statusl}->set_text_with_mnemonic("  $links _Back links");

	$self->{history}->write if $self->{history};
}

=item C<load_date(DAY, MONTH, YEAR)>

Load the zim page coresponding to this date.

=cut

sub load_date {
	my ($self, $day, $month, $year) = @_;
	#warn "User selected: $day, $month, $year\n";
	$year -= 1900;
	my $name = strftime('%Y_%m_%d', 0, 0, 0, $day, $month, $year);
	$name = $self->{settings}{cal_namespace}.$name;
	$name = $self->{repository}->resolve_name($name);
	$self->load_page($name);
}

=back

=head2 Actions

The following methods map directly to the ui actions in the menu-
and toolbars.

=over 4

=item C<NewPage(PAGE)>

Open a dialog which allows you to enter a name for a new page.
PAGE is used to fill in the namespace in the dialog.

=cut

sub NewPage {
	my ($self, $ns) = @_;

	if (defined $ns) { $ns =~ s/[^:]+$// }
	else { $ns = $self->{page}->namespace }
	$ns = '' if $ns eq ':';

	my $values = $self->run_prompt(
		'New page',
		['page'], {page => ['Page name', 'page', $ns]},
		'gtk-new', undef,
		"Note that linking to a non-existing page\nalso automatically creates a new page.\n"
	);
	return unless $values;
	my ($page) = @$values;
	return unless $page =~ /\S/;
	$self->JumpTo($page);
}

=item C<OpenRep(KEY)>

Open another repository. KEY can either be a file name or a repository name.
Without KEY the "open repository" dialog is prompted to the user.

=cut

sub OpenRep {
	my ($self, $key) = @_;
	unless (defined $key) {
		$self->RepositoriesDialog->show();
	}
	elsif ($key =~ '/' or -e $key) {
		$self->exec_new_window($key);
	}
	else {
		my $dir = Zim->get_repository($key);
		return $self->error_dialog("No such repository: $key")
			unless defined $dir;
		$self->exec_new_window($dir);
	}
}

=item C<Save()>

Force saving the current page and state.
Returns boolean for success.

=item C<SaveIfModified(SOFT)>

Save the page if it was modified.
Returns boolean for success or if page was not changed.

SOFT is an optional argument. If this argument is true the page may not be
saved if there is an issue. This is used to skip auto-saving in some cases.
Also "soft" changes may not be committed for version management etc.

=cut

sub Save {
	my $self = shift;
	warn "## GUI::Save\n";
	$self->_save_page(1, 1) or return 0;
	$self->save_config;
	return 1;
}

sub SaveIfModified {
	my ($self, $soft) = @_;
	return 1 if $self->{settings}{read_only}; # not modified, so save is ok
	return 0 if $self->{save_lock} and $soft;
	
	my $modified = $self->{objects}{PageView}->modified;
	
	warn "## GUI::SaveIfModified\n";
	return $self->_save_page($modified, !$soft);
}

# TODO part of this logic belongs in the PageView component

sub _save_page {
	my ($self, $save, $commit) = @_;
	$self->{save_lock} = 1; # Used to block autosave while "could not save" dialog
	
	my $page = $self->{page};
	eval {
		$self->PageView->save_page($page) if $save;
		$page->commit_change() if $commit;
	};
	if ($@) {
		my $error = $@;
		warn 'ERROR: '.$error;
		my $answer = $self->prompt_question(
			'Could not save - Zim', 'error',
			"<b>Could not save page</b>\n\n$error",
			['cancel', 'gtk-cancel', '_Cancel' ],
			['discard', 'gtk-delete', '_Discard changes'],
			['copy', 'gtk-save-as', '_Save a copy...' ],
			3 );
		if ($answer eq 'discard') {
			$self->{page}->discard_change;
			$self->_load_page($self->{page}); # load page wihtout saving
		}
		elsif ($answer eq 'copy') {
			return $self->SaveCopy; # recurs indirectly
		}
		else { return 0 } # return _without_ removing save_lock
	}

	$self->update_status();
	$self->{save_lock} = 0;
	return 1;
}

=item C<SaveCopy()>

=cut

sub SaveCopy {
	my $self = shift;
	return unless $self->{page};

	# prompt page
	my $new = $self->{page}->name . '_(Copy)' ;
	my $val = $self->run_prompt(
		'Save Copy - Zim',
		['page'], {page => ['Page', 'page', $new]},
		'gtk-save-as', '_Save Copy',
		'Please enter a name to save' );
	return unless defined $val;
	($new) = @$val;
	return unless length $new;

	my $l = $self->check_page_input($new);
	my $page = $self->{repository}->resolve_page($l);
	return $self->error_dialog("Can not save to page: $l")
		unless $page;
	
	# check if page exists
	if ($page->exists()) {
		my $answer = $self->prompt_question(
			'Page exists - Zim', 'warning',
			"Page '$new'\nalready exists.",
			['cancel', 'gtk-cancel', undef],
			['overwrite', undef, '_Overwrite'] );
		return unless $answer eq 'overwrite';
	}
	
	# change page and save buffer
	$self->{page} = $page;
	$self->Save;
	$self->Reload;
}

=item C<Export()>

=cut

sub Export { pop->ExportDialog->show() }

=item C<EmailPage()>

Open the current page in the email client.

=cut

sub EmailPage {
	my $self = shift;
	$self->SaveIfModified or return;
	my $subject = $self->{page}->name;
	my $io = $self->{page}->get_text;
	my $src = join '', <$io>;
	$src =~ s/(\W)/sprintf '%%%02x', ord $1/eg; # url encoding
	$self->open_url("mailto:?subject=$subject&body=".$src);
}

=item C<RenamePage(FROM, TO, UPDATE_SELF, UPDATE_OTHER)>

Wrapper for C<< Zim->move_page() >>.

Move page from FROM to TO. If TO is undefined a dialog is shown to ask for a
page name.

UPDATE_SELF is a boolean telling to update all links in this page.
UPDATE_OTHER is a boolean telling to update all links to this page.

Without arguments prompts the user for input.

Returns boolean for success.

=cut

sub RenamePage {
	my ($self, $from, $to, @update) = @_;

	$from = $self->{page}->name unless defined $from;
	
	my $move_current = $self->{page}->equals($from);
	my $save = $self->SaveIfModified(!$move_current);
	return 0 if $move_current and !$save;
	
	unless (defined $to) {
		($to, @update) = $self->prompt_rename_page_dialog($from);
		$self->check_page_input($to);
		return 0 unless defined $to;
		my $t = $self->{repository}->resolve_page($to);
		if ($t eq $from) { # Case sensitive move
			# FIXME call to resolve case sensitive
			# this one only works for absolute names
			$to = $self->{repository}->get_page($to);
		}
		else { $to = $t }
	}

	# Get backlinks
	my $rfrom = $move_current ? $self->{page} : $self->{repository}->get_page($from); # FIXME ugly lookup
	my @backlinks = $rfrom->list_backlinks();
	
	# Move page
	#warn "Move '$from' to '$to'\n";
	eval { $self->{repository}->move_page($from, $to) };
	if ($@) {
		$self->error_dialog("Could not rename $from to $to\n\n$@");
		return 0;
	}

	if (grep $_, @update) { # Update backlinks
		my ($dialog, $bar, $label) = $self->new_progress_bar(
			"Updating links", 'Updating..' );
		my $continue = 1;
		$dialog->signal_connect(response => sub {$continue = 0});
		my $i = 1;
		my $callback = sub {
			my $page = shift;
			$bar->pulse;
			$label->set_text('Updating links in ' . $page);
			while (Gtk2->events_pending) { Gtk2->main_iteration_do(0) }
			return $continue;
		};
		eval {
			if ($update[0]) { # UPDATE_SELF
				$callback->($to);
				$self->{repository}->get_page($to)
					->update_links_self($from);
			}
			if ($update[1]) { # UPDATE_OTHER
				for (@backlinks) {
					$callback->($_) or last;
					$self->{repository}->get_page($_)
						->update_links(	$from => $to );
				}
			}
		};
		$self->error_dialog($@) if $@;
		$dialog->destroy;
	}
	
	warn "# moved $from => $to\n";
	$self->{history}->delete_recent("$from") if $self->{history};
	$self->signal_emit('page_renamed', $from => $to);
	$self->load_page($to) if $move_current;
	
	$self->update_status;
	return 1;
}

=item C<DeletePage(PAGE, RECURS, NO_CONFIRM)>

Wrapper for C<< Zim->delete_page >>.
Asks the user for confirmation.

If PAGE is undefined the current page is deleted.

TODO: make recusive delete work

=cut

sub DeletePage {
	# FIXME option to delete a complete sub-tree
	# FIXME integrate with the save_page() logic, now we have 
	# redundant code here
	my ($self, $page, undef, $noconf) = @_;
	$page = $self->{page}->name unless defined $page;
	
	my $name = ref($page) ? $page->name : $page;
	my $delete_current = ($name eq $self->{page}->name);
	$self->SaveIfModified(!$delete_current);
	
	unless ($noconf) {
		my $r = $self->prompt_question(
			'Delete page - Zim', 'warning',
			"Are you sure you want to\ndelete '$name' ?",
			['no',  'gtk-cancel', undef],
			['yes', 'gtk-delete', undef]  );
		return unless $r eq 'yes';
	}
	
	$page = $self->{page} if $delete_current; # make sure we have the right object
	eval { $self->{repository}->delete_page($page) };
	return $self->error_dialog("Could not delete $name\n\n$@") if $@;

	# TODO trigger this with signals
	$self->{history}->delete_recent($name) if $self->{history};
	$self->signal_emit('page_deleted', $name);

	$self->load_page($page) if $delete_current;

	$self->update_status;
}

=item C<Props()>

Show the properties dialog.

=cut

sub Props { $_[0]->PropertiesDialog->show }

=item C<Close()>

Close the application.

=item C<Quit()>

Quit the application.

=cut

sub Close {
	my $self = shift;
	$self->on_delete_event(@_) && return;
	Gtk2->main_quit;
}

sub Quit { $_[0]->Close('FORCE') }

=item C<CopyLocation(PAGE, ...)>

Copies a list of page names to the clipboard.
Without argument uses the current page.

=cut

sub CopyLocation {
	my ($self, @pages) = @_;
	@pages = map {ref($_) ? $_->name : $_} @pages;
	unless (@pages) {
		@pages = $self->PageView->has_focus ? $self->{page}->name :
		         $self->TreeView->has_focus ? $self->TreeView->get_selected : undef ;
	}
	my $clipboard = Gtk2::Clipboard->get(
		Gtk2::Gdk::Atom->new('CLIPBOARD') );
	$clipboard->set_with_data(
		\&_clipboard_get, \&_clipboard_clear, \@pages,
		['text/x-zim-page-list', [], 0],
		$self->list_text_targets(1) );
}

sub _clipboard_get {
	my ($clipboard, $selection, $id, $list) = @_;
	#print STDERR '_clipboard_get type: '.$selection->target->name."\n";

	if ($selection->target->name eq 'text/x-zim-page-list') {
		my $data = Zim::GUI::Component->encode_uri_list(@$list);
		my $type = Gtk2::Gdk::Atom->new('text/x-zim-page-list');
		$selection->set($type, 8, $data);
	}
	else { # text
		my $text = join '', map "$_\n", @$list;
		$selection->set_text($text);
	}
}

sub _clipboard_clear { } # do nothing

=item C<Prefs()>

Show the preferences dialog.

=cut

sub Prefs { shift->PreferencesDialog->show }

=item C<TToolBar(BOOL)>

Toggel toolbar visibility.
If BOOL is undefined it will just toggle the current state.

=cut

sub TToolBar {
	my ($self, $show) = @_;
	$show = $self->{toolbar}->visible ? 0 : 1 unless defined $show;
	$show ? $self->{toolbar}->show_all : $self->{toolbar}->hide_all ;
	$self->{settings}{toolbar_vis} = $show;
	$self->actions_show_active(TToolBar => $show);
}

=item C<TStatusBar(BOOL)>

Toggle statusbar visibility.
If BOOL is undefined it will just toggle the current state.

=cut

sub TStatusBar {
	my ($self, $show) = @_;
	$show = $self->{statusbar}->visible ? 0 : 1 unless defined $show;
	$show ? $self->{statusbar}->show_all : $self->{statusbar}->hide_all ;
	$self->{settings}{statusbar_vis} = $show;
	$self->actions_show_active(TStatusBar => $show);
}

=item C<TPane(BOOL)>

Toggle visibility of the side pane.
If BOOL is undefined it will just toggle the current state.
If BOOL is "-1" the pane will be shown, but hidden again as soon
as a page is selected.

=cut

sub TPane {
	my ($self, $show) = @_;
	$show = $self->{l_vbox}->visible ? 0 : 1 unless defined $show;

	$self->{_pane_visible} = $show;
	my $widget = $self->{l_vbox};
	my $hpaned = $self->{hpaned};
	if ($show) {
		$hpaned->set_position($self->{settings}{pane_pos});
		my $tree_view = $self->TreeView; # possibly autoloaded here
		$widget->show_all;
		$tree_view->grab_focus;
	}
	else {
		$self->{settings}{pane_pos} = $hpaned->get_position();
		$widget->hide_all;
		$self->PageView->grab_focus;
	}

	$self->{settings}{pane_vis} = $show unless $show == -1;
	$self->actions_show_active(TPane => $show);
}

=item C<TPathBar(TYPE)>

Set the pathbar type to TYPE.

=cut


sub on_TPathBar {
	# get the type from the action name
	my $self = pop;
	return if $self->{_block_actions};
	my $type = lc pop->get_name;
	$type =~ s/^pb//;
	$self->TPathBar($type);
}

sub TPathBar {
	my ($self, $type) = @_;
	return warn "BUG: No pathbar type given" unless defined $type;
	
	my $path_bar = $self->{objects}{PathBar};
	if (grep {$type eq $_} qw/recent history namespace/) {
		$path_bar->widget->show_all;
		$path_bar->set_type($type);
	}
	elsif ($type eq 'hidden') {
		$path_bar->widget->hide_all;
		$path_bar->clear_items;
	}
	else { warn "BUG: unknown pathbar_type: $type" }

	$self->{settings}{pathbar_type} = $type;
	$self->message("Pathbar type: $type");
	$self->actions_show_active('PB'.ucfirst($type) => 1);
}

=item C<TCalendar(BOOL)>

Toggle calendar visibility.
If BOOL is undefined it will just toggle the current state.

=cut

sub TCalendar {
	my ($self, $show) = @_;
	my $cal = $self->Calendar;
	$show = $cal->visible ? 0 : 1 unless defined $show;
	$show ? $cal->show : $cal->hide ;
	$self->{settings}{show_cal} = $show;
}

=item C<Reload()>

Save and reload the current page.

=cut

sub Reload { $_[0]->load_page( "$_[0]->{page}" ) if $_[0]->{page} }


=item C<Search($QUERY)>

Open QUERY in the search dialog.

=cut

sub Search { $_[0]->SearchDialog->search($_[1]) }

=item C<SearchBL()>

Open backlinks for current page in search dialog.

=cut

sub SearchBL {
	my $self = shift;
	$self->SearchDialog->search($self->{page}->name);
}

=item C<OpenFolder()>

Open the dir for the current page.

=cut

sub OpenFolder {
	my $self = shift;
	my $dir = $self->{page}->properties->{base};
	return unless length $dir;
	#warn "Opening $dir\n";
	$self->open_directory($dir);
}

=item C<RBIndex()>

Rebuild the index cache.

=cut

sub RBIndex {
	my $self = shift;
	$self->{repository}->_flush_cache;
	my $tree = $self->TreeView;
	$tree->{_loaded} = 0;
	$tree->load_index;
	$self->Reload;
}


=item C<GoBack($INT)>

Go back one or more steps in the history stack.

=item C<GoForward($INT)>

Go forward one or more steps in the history stack.

=cut

sub GoBack {
	my $self = shift;
	my $i = shift || 1;
	return unless $self->{history};
	my $rec = $self->{history}->back($i) || return;
	$self->load_page($rec);
}

sub GoForward {
	my $self = shift;
	my $i = shift || 1;
	return unless $self->{history};
	my $rec = $self->{history}->forw($i) || return;
	$self->load_page($rec);
}

=item C<GoParent()>

Go to page up in namespace.

=item C<GoChild()>

Go to page down in namespace.

=cut

sub GoParent {
	my $self = shift;
	my $namespace = $self->{page}->namespace;
	return if $namespace eq ':';
	$namespace =~ s/:+$//;
	$self->load_page($namespace);
}

sub GoChild {
	my $self = shift;
	return unless $self->{history};
	my $namespace = $self->{history}->get_namespace;
	my $name = $self->{page}->name;
	return unless $namespace =~ /^(:*\Q$name\E:+[^:]+)/;
	$self->load_page($1);
}

=item C<GoNext()>

Go to the next page in the index.

=item C<GoPrev()>

Go to the previous page in the index.

=cut

sub GoNext {
	my $self = shift;
	my $page = $self->{page}->get_next;
	$self->load_page($page) if $page;
}

sub GoPrev {
	my $self = shift;
	my $page = $self->{page}->get_prev;
	$self->load_page($page) if $page;
}

=item C<GoToday()>

Go to the page for todays date.

=cut

sub GoToday {
	my $self = shift;
	my ($day, $month, $year) = ( localtime )[3, 4, 5];
	$year += 1900;
	$self->load_date($day, $month, $year);
}

=item C<GoHome()>

Go to the home page.

=cut

sub GoHome { $_[0]->load_page($_[0]->{home}) }

=item C<JumpTo(PAGE)>

Go to PAGE. Shows a dialog when no page is given.

=cut

sub JumpTo {
	my ($self, $page) = @_;

	unless (defined $page) {
		my $ns = $self->{page}->namespace;
		$ns = '' if $ns eq ':';
		my $values = $self->run_prompt(
			'Jump to',
			['page'], {page => ['Jump to Page', 'page', $ns]},
			'gtk-jump-to', undef, undef );
		return unless $values;
		($page) = @$values;
		return unless $page =~ /\S/;
	}
	elsif (ref $page) {
		return $self->load_page($page);
	}

	$page = $self->check_page_input($page);
	return unless defined $page;

	$self->load_page(
		$self->{repository}->resolve_name($page, $self->{page}) );
}

=item C<ShowHelpKeys()>

=item C<ShowHelpBugs()>

Shows help on keybindings and bugs. Trying to seduce people to actually read
the manual...

=cut

sub ShowHelpKeys { $_[0]->ShowHelp(':zim:usage:keybindings') }

sub ShowHelpBugs { $_[0]->ShowHelp(':zim:bugs') }

=item C<About()>

This dialog tells you about the version of zim you are using.

=cut


=begin comment

sub About {
	# For Gtk > 2.6.0
	my $self = shift;
	my %info = (
		name => 'Zim',
		version => $Zim::VERSION,
		copyright => $Zim::COPYRIGHT,
		license => $Zim::LONG_VERSION,
		authors => $Zim::AUTHORS,
		logo => Gtk2::Gdk::Pixbuf->new_from_file($Zim::ICON),
		# logo-icon-name => 'zim',
		comments => 'A desktop wiki',
		website => $Zim::WEBSITE,
	);
	my $hook = sub {$self->link_clicked($_[1])};
	Gtk2::AboutDialog->set_url_hook($hook);
	Gtk2::AboutDialog->set_email_hook($hook);
	Gtk2->show_about_dialog($self->{window}, %info);
}

=end comment

=cut

sub About {
	my $self = shift;
	
	my $dialog = Gtk2::Dialog->new(
		'About Zim', $self->{window},
	       	[qw/modal destroy-with-parent no-separator/],
		'gtk-close' => 'close',
	);
	$dialog->set_resizable(0);
	$dialog->set_border_width(5);
	$dialog->set_icon($self->{window}->get_icon);
	my $button = $self->new_button('gtk-help', '_More');
	$dialog->add_action_widget($button, 'help');
	$dialog->set_default_response('close');
	
	$dialog->vbox->add(
		Gtk2::Image->new_from_file($Zim::ICON) );
		# defined in Component.pm

	my $text = $Zim::LONG_VERSION;
	$text =~ s/^(.*)$/\n<b>$1<\/b>/m;
	my $label = Gtk2::Label->new();
	$label->set_markup($text);
	$label->set_justify('center');
	$dialog->vbox->add($label);
	
	$dialog->show_all;
	my $response = $dialog->run;
	$dialog->destroy;

	$self->ShowHelp('zim:about') if $response eq 'help';
}

=back

=head2 Other functions

Functions below are used by other methods.
They are not considered part of the api.

=over 4

=item C<message(STRING)>

Flash a message in the statusbar for a short time.

=cut

sub message {
	my ($self, $str) = @_;
	return if $self->{message_lock};
	my $timeout = $self->{_message_timeout};
	$self->push_status($str, 'message');
	Glib::Source->remove($timeout) if $timeout >= 0;
	$timeout = Glib::Timeout->add(2500, sub {
		$self->pop_status('message');
		$self->{_message_timeout} = -1;
		return 0; # removes timeout
	} );
	$self->{_message_timeout} = $timeout;
}

=item C<update_status()>

Sets the statusbar to display the current page name and some other
information.

=cut

sub update_status {
	my $self = shift;
	my $stat = ' '.$self->{page}->name;
	$stat .= '*' if $self->PageView->modified;
	if ($_ = $self->{page}->status()) { $stat .= '  -  '.uc($_) }
	$stat .= ' [readonly]' if $self->{settings}{read_only};
	$self->push_status($stat, 'page');
}

=item C<push_status(STRING, CONTEXT)>

Put STRING in the status bar.

=cut

sub push_status {
	my ($self, $str, $id) = @_;
	my $statusbar = $self->{status1};
	$id = $statusbar->get_context_id($id);
	$statusbar->pop($id);
	$statusbar->push($id, $str);
}

=item pop_status(CONTEXT)

Removes a string from the status bar.

=cut

sub pop_status {
	my ($self, $id) = @_;
	my $statusbar = $self->{status1};
	$id = $statusbar->get_context_id($id);
	$statusbar->pop($id);
}

=item C<prompt_rename_page_dialog(PAGE)>

Runs a dialog that gathers info for moving a page.
Returns new page name and update boolean.

=cut

sub prompt_rename_page_dialog {
	my ($self, $page) = @_;
	
	my ($dialog, $entries) = $self->new_prompt(
		'Rename page',
		['page'], {page => ['Rename to', 'page', $page]},
		'gtk-save', '_Rename'  );
	
	my ($entry) = @$entries;
	$entry->select_region(length($1), -1) if $page =~ /^(.+:)/;

	my $rpage = ($page eq $self->{page}->name) ? $self->{page} : $self->{repository}->get_page($page); # FIXME ugly lookup
	my $nlinks = scalar $rpage->list_backlinks;
	my $check1 = Gtk2::CheckButton->new("_Update links in this page");
	my $check2 = Gtk2::CheckButton->new("_Update $nlinks pages linking here");
	$check1->set_active(1);
	if ($nlinks > 0) { $check2->set_active(1)    }
	else             { $check2->set_sensitive(0) }
	$check1->show;
	$check2->show;
	$dialog->vbox->add($check1);
	$dialog->vbox->add($check2);
	
	if ($dialog->run eq 'ok') { $page = $entry->get_text }
	else                      { $page = undef            }
	my $up1 = $check1->get_active;
	my $up2 = $check2->get_active;
	$dialog->destroy;

	$page = undef unless $page =~ /\S/;
	return defined($page) ? ($page, $up1, $up2) : ();
}

1;

__END__

=back

=head1 BUGS

Please mail the author if you find any bugs.

=head1 AUTHOR

Jaap Karssenberg (Pardus) E<lt>pardus@cpan.orgE<gt>

Copyright (c) 2005 Jaap G Karssenberg. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 SEE ALSO

L<zim>(1),
L<Zim>,
L<Zim::GUI::Component>

=cut

